package gosu;

import javax.imageio.ImageIO;
import java.io.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.RGBImageFilter;
import java.awt.image.ImageFilter;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.font.LineBreakMeasurer;
import java.awt.Toolkit;
import java.text.AttributedString;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.awt.image.ColorConvertOp;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;
import com.sun.opengl.util.ImageUtil;
import javax.media.opengl.GL;

public class Image {
  public static int DEFAULT = 0;
  public static int ADDITIVE = 1;

  public static int JUSTIFY = 0;
  public static int RIGHT   = 1;
  public static int LEFT    = 2;
  public static int CENTER  = 3;

  private Window _window;
  private int _width;
  private int _height;
  private BufferedImage _image;
  private Texture _texture;

  private boolean _softBorders;

  public Image(Window window, String filename) {
    this(window, filename, false);
  }


  public Image(Window window, String filename, boolean hardBorders) {
    this(window, loadImage(filename), hardBorders);
  }


  public Image(Window window, String filename, boolean hardBorders, int srcX, int srcY, int srcWidth, int srcHeight) {
    _window = window;
    _width = srcWidth;
    _height = srcHeight;
    BufferedImage image = null;

    if (srcWidth == 0 || srcHeight == 0) {
      // Make an image consisting only of transparent borders
      image = getCompatibleImage(srcWidth + 2, srcHeight + 2);
    } else {
      image = loadImage(filename);

      // If this is a .bmp image, then change the color ff00ff (fuchsia) to transparent
      if (!image.getColorModel().hasAlpha()) {
        image = makeFuchsiaTransparent(image);
      }

      image = applyBorders(image.getSubimage(srcX, srcY, srcWidth, srcHeight), hardBorders);
      _softBorders = !hardBorders;
    }

    _image = image;
  }


  public Image(Window window, BufferedImage image, boolean hardBorders) {
    _window = window;

    // If this is a .bmp image, then change the color ff00ff (fuchsia) to transparent
    if (!image.getColorModel().hasAlpha()) {
      image = makeFuchsiaTransparent(image);
    }

    _width = image.getWidth();
    _height = image.getHeight();
    image = applyBorders(image, hardBorders);
    _softBorders = !hardBorders;
    _image = image;
  }


  boolean hasSoftBorders() { return _softBorders; }


  /**
   * Lazily creates the texture.
   */
  Texture getTexture() {
    if (_texture == null) {
      _texture = TextureIO.newTexture(_image, false);
      //_texture.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
    }

    return _texture;
  }


  public void draw(double x, double y, double z) {
    _window.addDrawOp(new ImageDrawOp(this, x, y, z));
  }


  public void draw(double x, double y, double z, double factorX, double factorY) {
    _window.addDrawOp(new ImageDrawOp(this, x, y, z, factorX, factorY));
  }


  public void draw(double x, double y, double z, double factorX, double factorY, Color color, int mode) {
    _window.addDrawOp(new ImageDrawOp(this, x, y, z, factorX, factorY, color, mode));
  }


  public void drawRot(double x, double y, double z, double angle) {
    _window.addDrawOp(new ImageDrawRotOp(this, x, y, z, angle));
  }


  public void drawRot(double x, double y, double z, double angle, double centerX, double centerY) {
    _window.addDrawOp(new ImageDrawRotOp(this, x, y, z, angle, centerX, centerY));
  }


  public void drawRot(double x, double y, double z, double angle, double centerX, double centerY, double factorX, double factorY) {
    _window.addDrawOp(new ImageDrawRotOp(this, x, y, z, angle, centerX, centerY, factorX, factorY));
  }


  public void drawRot(double x, double y, double z, double angle, double centerX, double centerY, double factorX, double factorY, Color color, int mode) {
    _window.addDrawOp(new ImageDrawRotOp(this, x, y, z, angle, centerX, centerY, factorX, factorY, color, mode));
  }


  public void drawAsQuad(double x1, double y1, Color c1,
                         double x2, double y2, Color c2,
                         double x3, double y3, Color c3,
                         double x4, double y4, Color c4, double z) {
    drawAsQuad(x1, y1, c1, x2, y2, c2, x3, y3, c3, x4, y4, c4, z, Image.DEFAULT);
  }


  public void drawAsQuad(double x1, double y1, Color c1,
                         double x2, double y2, Color c2,
                         double x3, double y3, Color c3,
                         double x4, double y4, Color c4,
                         double z, int mode) {
    _window.addDrawOp(new ImageDrawOp(this, x1, y1, c1, x2, y2, c2, x3, y3, c3, x4, y4, c4, z, mode));
  }


  /**
   * Convenience method that splits up an image into tiles.
   *
   * @param tileWidth If positive, specifies the width of one tile in pixels. If negative,
   * the image is divided into <code>-tileWidth</code> rows.
   *
   * @param tileHeight If positive, specifies the height of one tile in pixels. If negative,
   * the image is divided into <code>-tileHeight</code> columns.
   */
  public static ArrayList<Image> loadTiles(Window window, String filename, int tileWidth, int tileHeight, boolean hardBorders) {
    BufferedImage image = loadImage(filename);
    ArrayList<Image> list = new ArrayList<Image>();
    
    int tilesX, tilesY;

    if (tileWidth > 0) {
      tilesX = image.getWidth() / tileWidth;
    } else {
      tilesX = -tileWidth;
      tileWidth = image.getWidth() / tilesX;
    }
    
    if (tileHeight > 0) {
      tilesY = image.getHeight() / tileHeight;
    } else {
      tilesY = -tileHeight;
      tileHeight = image.getHeight() / tilesY;
    }
    
    for (int y = 0; y < tilesY; y++) {
      for (int x = 0; x < tilesX; x++) {
        list.add(new Image(window, image.getSubimage(x * tileWidth, y * tileHeight, tileWidth, tileHeight), hardBorders));
      }
    }

    return list;
  }


  /**
   * Creates a bitmap that is filled with the text given to the function.
   * The text may contain line breaks.
   * @param fontName Name of a system font, or a filename to a TTF file (must contain '/').
   * @param fontHeight Height of the font in pixels.
   * @param lineSpacing Spacing between two lines of text in pixels.
   * @param maxWidth Maximal width of the bitmap that will be returned. Text
   * will be split into multiple lines to avoid drawing over the right
   * border. When a single word is too long, it will be truncated.
   */
  public static Image fromText(Window window, String text, String fontName, int fontHeight, float lineSpacing, float maxWidth, int align) {
    java.awt.Font font = new java.awt.Font(fontName, java.awt.Font.PLAIN, fontHeight);
    ArrayList<TextLayout> layouts = new ArrayList<TextLayout>();

    // Find out the height of the image
    int height = 0;
    for (String line : text.split("\n")) {
      if (line.length() == 0) line = " ";
      AttributedString as = new AttributedString(line, font.getAttributes());

      LineBreakMeasurer lbm = new LineBreakMeasurer(as.getIterator(), new FontRenderContext(null, true, false));
      while (lbm.getPosition() < line.length()) {
        TextLayout layout = lbm.nextLayout(maxWidth);
        height += layout.getAscent() + layout.getDescent() + lineSpacing;
        layouts.add(layout);
      }
    }
    height += fontHeight;

    BufferedImage image = getCompatibleImage((int) maxWidth, height);
    Graphics2D g = (Graphics2D) image.getGraphics();

    float x = 0;
    float y = 0;
    for (TextLayout layout : layouts) {
      y += layout.getAscent();
      float dx = 0;
      if (align == JUSTIFY) {
        layout = layout.getJustifiedLayout(maxWidth);
      } else if (align == RIGHT) {
        dx = maxWidth - layout.getVisibleAdvance();
      } else if (align == CENTER) {
        dx = (maxWidth - layout.getVisibleAdvance()) / 2.0f;
      }

      layout.draw(g, x + dx, y);
      y += layout.getDescent() + lineSpacing;
    }

    return new Image(window, image, false);
  }


  public static Image fromText(Window window, String text, String fontName, int fontHeight) {
    java.awt.Font font = new java.awt.Font(fontName, java.awt.Font.PLAIN, fontHeight);

    // Get the width of the image
    // Add an extra character's width: thus the "x"
    int width = (int) font.createGlyphVector(new FontRenderContext(null, true, false), text + "x").getVisualBounds().getWidth();
    BufferedImage image = getCompatibleImage(width, fontHeight);
    Graphics2D g = (Graphics2D) image.getGraphics();
    g.setFont(font);
    g.drawString(text, 0, fontHeight);
    return new Image(window, image, false);
  }


  public int getWidth() { return _width; }

  public int getHeight() { return _height; }


  private static BufferedImage makeFuchsiaTransparent(java.awt.Image image) {
    ImageFilter alphaFilter = new RGBImageFilter() {
      public int filterRGB(int x, int y, int rgb) {
        if (rgb == 0xffff00ff) return 0;
        return rgb;
      }
    };
    FilteredImageSource fis = new FilteredImageSource(image.getSource(), alphaFilter);
    return convertToBufferedImage(Toolkit.getDefaultToolkit().createImage(fis));
  }


  /**
   * Applies soft or hard borders to an image.
   */
  private static BufferedImage applyBorders(BufferedImage image, boolean hardBorders) {
    if (hardBorders) return image;

    int width = image.getWidth();
    int height = image.getHeight();
    BufferedImage newImage = getCompatibleImage(width + 2, height + 2);
    newImage.createGraphics().drawImage(image, 1, 1, null);

    /*
    if (hardBorders) {
      // duplicate the original image's borders

      // top
      newImage.setRGB(1, 0, width, 1, image.getRGB(0, 0, width, 1, null, 0, width), 0, width);

      // bottom
      newImage.setRGB(1, height + 1, width, 1, image.getRGB(0, height - 1, width, 1, null, 0, width), 0, width);

      // left
      newImage.setRGB(0, 1, 1, height, image.getRGB(0, 0, 1, height, null, 0, 1), 0, 1);

      // right
      newImage.setRGB(width + 1, 1, 1, height, image.getRGB(width - 1, 0, 1, height, null, 0, 1), 0, 1);

      // top left
      newImage.setRGB(0, 0, image.getRGB(0, 0));

      // top right
      newImage.setRGB(width + 1, 0, image.getRGB(width - 1, 0));

      // bottom left
      newImage.setRGB(0, height + 1, image.getRGB(0, height - 1));

      // bottom right
      newImage.setRGB(width + 1, height + 1, image.getRGB(width - 1, height - 1));
    }
    */

    return newImage;
  }


  private static BufferedImage convertToBufferedImage(java.awt.Image image) {
    BufferedImage bImage = getCompatibleImage(image.getWidth(null), image.getHeight(null));
    bImage.getGraphics().drawImage(image, 0, 0, null);
    return bImage;
  }


  private static BufferedImage getCompatibleImage(int width, int height) {
    return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()
      .createCompatibleImage(width, height, Transparency.TRANSLUCENT);
    //return ImageUtil.createCompatibleImage(image.getWidth(null), image.getHeight(null));
    //return _window.getGraphics().getDeviceConfiguration().createCompatibleImage(image.getWidth(null), image.getHeight(null), Transparency.TRANSLUCENT);
    //return new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
  }


  private static BufferedImage loadImage(String filename) {
    BufferedImage image;
    try {
      InputStream is = Image.class.getResourceAsStream("/" + filename);
      if (is == null) {
        is = new FileInputStream(new File(filename));
      }

      image = ImageIO.read(is);
    } catch (IOException e) {
      //TODO: Pick a better exception to throw or make one
      throw new RuntimeException("Could not read image file: " + filename);
    }
    return image;
  }
}
